Skip to content

feat(cloudflare): Split alarms into multiple traces and link them#19373

Open
JPeer264 wants to merge 3 commits intodevelopfrom
jp/split-alarm
Open

feat(cloudflare): Split alarms into multiple traces and link them#19373
JPeer264 wants to merge 3 commits intodevelopfrom
jp/split-alarm

Conversation

@JPeer264
Copy link
Copy Markdown
Member

@JPeer264 JPeer264 commented Feb 18, 2026

closes #19105
closes JS-1604

closes #19453
closes JS-1774

This actually splits up alarms into its own traces and binding them with span links. It also adds the setAlarm, getAlarm and deleteAlarm instrumentation, which is needed to make this work.

The logic works as following. When setAlarm is getting called it will store the alarm inside the durable object. Once the alarm is being executed the previous trace link will be retrieved via ctx.storage.get and then set as span link. Using the durable object itself as storage between alarms is even used on Cloudflare's alarm page.

Also it is worth to mention that only 1 alarm at a time can happen, so it is safe to use a fixed key for the previous trace. I implemented the trace links, so they could be reused in the future for other methods as well, so they are not exclusively for alarms.

Example alarm that triggers 3 new alarms to show the span links: https://sentry-sdks.sentry.io/explore/traces/trace/1ef3f388601b425d96d1ed9de0d5b7b4/

@JPeer264 JPeer264 self-assigned this Feb 18, 2026
@JPeer264 JPeer264 changed the title ref(cloudflare): Move internal files and functions around feat(cloudflare): Split alarms into multiple traces and link them Feb 18, 2026
@linear
Copy link
Copy Markdown

linear bot commented Feb 18, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 18, 2026

Codecov Results 📊


Generated by Codecov Action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 18, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.65 kB +0.02% +5 B 🔺
@sentry/browser - with treeshaking flags 24.14 kB +0.03% +5 B 🔺
@sentry/browser (incl. Tracing) 42.16 kB +0.02% +7 B 🔺
@sentry/browser (incl. Tracing, Profiling) 46.77 kB +0.02% +9 B 🔺
@sentry/browser (incl. Tracing, Replay) 80.94 kB +0.01% +5 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.56 kB +0.01% +5 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 85.66 kB +0.01% +8 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 97.92 kB +0.01% +5 B 🔺
@sentry/browser (incl. Feedback) 42.42 kB +0.02% +6 B 🔺
@sentry/browser (incl. sendFeedback) 30.31 kB +0.02% +6 B 🔺
@sentry/browser (incl. FeedbackAsync) 35.3 kB +0.05% +17 B 🔺
@sentry/browser (incl. Metrics) 26.96 kB +0.03% +7 B 🔺
@sentry/browser (incl. Logs) 27.11 kB +0.03% +7 B 🔺
@sentry/browser (incl. Metrics & Logs) 27.78 kB +0.03% +7 B 🔺
@sentry/react 27.41 kB +0.03% +6 B 🔺
@sentry/react (incl. Tracing) 44.48 kB +0.02% +5 B 🔺
@sentry/vue 30.08 kB +0.02% +5 B 🔺
@sentry/vue (incl. Tracing) 44.05 kB +0.02% +8 B 🔺
@sentry/svelte 25.67 kB +0.02% +5 B 🔺
CDN Bundle 28.33 kB +0.05% +14 B 🔺
CDN Bundle (incl. Tracing) 43.13 kB +0.06% +25 B 🔺
CDN Bundle (incl. Logs, Metrics) 29.7 kB +0.05% +14 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 44.17 kB +0.05% +19 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 68.5 kB +0.03% +17 B 🔺
CDN Bundle (incl. Tracing, Replay) 80.02 kB +0.03% +19 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.06 kB +0.03% +18 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 85.56 kB +0.02% +15 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.59 kB +0.02% +16 B 🔺
CDN Bundle - uncompressed 82.72 kB +0.07% +54 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 127.86 kB +0.05% +54 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 86.86 kB +0.07% +54 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 131.27 kB +0.05% +54 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 209.84 kB +0.03% +54 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 244.74 kB +0.03% +54 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 248.13 kB +0.03% +54 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 257.65 kB +0.03% +54 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 261.04 kB +0.03% +54 B 🔺
@sentry/nextjs (client) 46.9 kB +0.02% +7 B 🔺
@sentry/sveltekit (client) 42.62 kB +0.02% +7 B 🔺
@sentry/node-core 55.77 kB +0.03% +16 B 🔺
@sentry/node 172.41 kB -0.2% -339 B 🔽
@sentry/node - without tracing 96.05 kB +0.05% +39 B 🔺
@sentry/aws-serverless 112.85 kB +0.07% +77 B 🔺

View base workflow run

Base automatically changed from jp/prepare-context-instrument to develop February 18, 2026 10:53
@JPeer264 JPeer264 force-pushed the jp/split-alarm branch 2 times, most recently from b949de1 to ce3a761 Compare February 20, 2026 10:40
@JPeer264 JPeer264 marked this pull request as ready for review February 20, 2026 10:59
const result = Reflect.apply(target, thisArg, args);
const executeSpan = (): unknown => {
return startSpan({ name: spanName, attributes, links }, async span => {
// TODO: Remove this once EAP can store span links. We currently only set this attribute so that we
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this is a 1:1 copy from here:

// TODO: Remove this once EAP can store span links. We currently only set this attribute so that we
// can obtain the previous trace information from the EAP store. Long-term, EAP will handle
// span links and then we should remove this again. Also throwing in a TODO(v11), to remind us
// to check this at v11 time :)
span.setAttribute(
PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE,
`${previousTraceSpanCtx.traceId}-${previousTraceSpanCtx.spanId}-${
spanContextSampled(previousTraceSpanCtx) ? 1 : 0
}`,
);

@JPeer264 JPeer264 marked this pull request as draft February 23, 2026 11:18
@JPeer264 JPeer264 force-pushed the jp/split-alarm branch 2 times, most recently from b866662 to e1f993f Compare February 23, 2026 14:22
@JPeer264 JPeer264 marked this pull request as ready for review February 23, 2026 14:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

This pull request has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you apply the label PR: no-auto-close I will leave it alone ... forever!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Cloudflare

  • Split alarms into multiple traces and link them by JPeer264 in #19373
  • Support basic WorkerEntrypoint by JPeer264 in #19884

Core

  • Support registerTool/registerResource/registerPrompt in MCP integration by betegon in #20071
  • Support embeddings in langchain by nicohrubec in #20017

Deps

  • Bump lodash.template from 4.5.0 to 4.18.1 by dependabot in #20085
  • Bump @xmldom/xmldom from 0.8.3 to 0.8.12 by dependabot in #20066

Other

  • (core, node) Portable Express integration by isaacs in #19928
  • (deno) Add denoRuntimeMetricsIntegration by chargome in #20023
  • (node, bun) Enforce minimum collection interval in runtime metrics integrations by chargome in #20068

Bug Fixes 🐛

  • (aws-serverless) Add timeout to _endSpan forceFlush to prevent Lambda hanging by logaretm in #20064
  • (cloudflare) Ensure every request instruments functions by JPeer264 in #20044
  • (core) Set span.status to error when MCP tool returns JSON-RPC error response by betegon in #20082
  • (gatsby) Fix errorHandler signature to match bundler-plugin-core API by JPeer264 in #20048

Internal Changes 🔧

Core

  • Do not emit spans for chats.create in google-genai by nicohrubec in #19990
  • Unify .do* span ops to gen_ai.generate_content by nicohrubec in #20074
  • Simplify addResponseAttributes in openai integration by nicohrubec in #20013
  • Extract shared endStreamSpan for AI integrations by nicohrubec in #20021
  • Remove provider-specific AI span attributes in favor of gen_ai attributes in sentry conventions by nicohrubec in #20011

Deps

  • Bump mshick/add-pr-comment from dd126dd8c253650d181ad9538d8b4fa218fc31e8 to e7516d74559b5514092f5b096ed29a629a1237c6 by dependabot in #20078
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.24.1 to 2.25.2 by dependabot in #20081

Other

  • (node) Add node integration tests for Vercel ToolLoopAgent by nicohrubec in #20087
  • (nuxt) Make Nuxt 5 (nightly) E2E optional by s1gr1d in #20113
  • Update validate-pr workflow by stephanie-anderson in #20072
  • Remove unused tsconfig-template folder by mydea in #20067

🤖 This preview updates automatically when you update the PR.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Redundant local SpanLink type duplicates @sentry/core export
    • Exported SpanLink from @sentry/core and replaced the local duplicate type definition with an import from core.
  • ✅ Fixed: Unnecessary teardown overhead for non-setAlarm storage methods
    • Moved teardown logic inside a conditional check so only setAlarm applies the .then() wrapper and waitUntil call, eliminating overhead for other storage methods.

Create PR

Or push these changes by commenting:

@cursor push 52d276ce19
Preview (52d276ce19)
diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
--- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
+++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
@@ -57,33 +57,35 @@
             },
           },
           () => {
-            const teardown = async (): Promise<void> => {
-              // When setAlarm is called, store the current span context so that when the alarm
-              // fires later, it can link back to the trace that called setAlarm.
-              // We use the original (uninstrumented) storage (target) to avoid creating a span
-              // for this internal operation. The storage is deferred via waitUntil to not block.
-              if (methodName === 'setAlarm') {
+            const result = (original as (...args: unknown[]) => unknown).apply(target, args);
+
+            // Only setAlarm needs teardown to store span context for trace linking
+            if (methodName === 'setAlarm') {
+              const teardown = async (): Promise<void> => {
+                // Store the current span context so that when the alarm fires later,
+                // it can link back to the trace that called setAlarm.
+                // We use the original (uninstrumented) storage (target) to avoid creating a span
+                // for this internal operation. The storage is deferred via waitUntil to not block.
                 await storeSpanContext(target, 'alarm');
+              };
+
+              if (!isThenable(result)) {
+                waitUntil?.(teardown());
+                return result;
               }
-            };
 
-            const result = (original as (...args: unknown[]) => unknown).apply(target, args);
-
-            if (!isThenable(result)) {
-              waitUntil?.(teardown());
-
-              return result;
+              return result.then(
+                res => {
+                  waitUntil?.(teardown());
+                  return res;
+                },
+                e => {
+                  throw e;
+                },
+              );
             }
 
-            return result.then(
-              res => {
-                waitUntil?.(teardown());
-                return res;
-              },
-              e => {
-                throw e;
-              },
-            );
+            return result;
           },
         );
       };

diff --git a/packages/cloudflare/src/utils/traceLinks.ts b/packages/cloudflare/src/utils/traceLinks.ts
--- a/packages/cloudflare/src/utils/traceLinks.ts
+++ b/packages/cloudflare/src/utils/traceLinks.ts
@@ -1,6 +1,6 @@
 import type { DurableObjectStorage } from '@cloudflare/workers-types';
 import { TraceFlags } from '@opentelemetry/api';
-import { getActiveSpan } from '@sentry/core';
+import { getActiveSpan, type SpanLink } from '@sentry/core';
 
 /** Storage key prefix for the span context that links consecutive method invocations */
 const SENTRY_TRACE_LINK_KEY_PREFIX = '__SENTRY_TRACE_LINK__';
@@ -12,16 +12,6 @@
   sampled: boolean;
 }
 
-/** Span link structure for connecting traces */
-export interface SpanLink {
-  context: {
-    traceId: string;
-    spanId: string;
-    traceFlags: number;
-  };
-  attributes?: Record<string, string>;
-}
-
 /**
  * Gets the storage key for a specific method's trace link.
  */

diff --git a/packages/cloudflare/src/wrapMethodWithSentry.ts b/packages/cloudflare/src/wrapMethodWithSentry.ts
--- a/packages/cloudflare/src/wrapMethodWithSentry.ts
+++ b/packages/cloudflare/src/wrapMethodWithSentry.ts
@@ -90,7 +90,10 @@
             // but the scope still holds a reference to it (e.g., alarm handlers in Durable Objects)
             // For startNewTrace, always create a fresh client
             if (startNewTrace || !scopeClient?.getTransport()) {
-              const client = init({ ...wrapperOptions.options, ctx: context as unknown as ExecutionContext | undefined });
+              const client = init({
+                ...wrapperOptions.options,
+                ctx: context as unknown as ExecutionContext | undefined,
+              });
               scope.setClient(client);
               scopeClient = client;
             }

diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -361,6 +361,7 @@
   XhrBreadcrumbHint,
 } from './types-hoist/breadcrumb';
 export type { ClientReport, Outcome, EventDropReason } from './types-hoist/clientreport';
+export type { SpanLink, SpanLinkJSON } from './types-hoist/link';
 export type {
   Context,
   Contexts,

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 43b8f06. Configure here.

e => {
throw e;
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary teardown overhead for non-setAlarm storage methods

Low Severity

The teardown closure, .then() wrapper, and waitUntil call are applied to every instrumented storage method (get, put, delete, list, getAlarm, deleteAlarm), but only setAlarm actually has teardown work. For all other methods, this creates a no-op async function, wraps every async result in an extra .then() hop, and passes an empty promise to waitUntil — all unnecessarily. This adds a microtask per storage operation.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 43b8f06. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 9,287 - 8,916 +4%
GET With Sentry 1,746 19% 1,697 +3%
GET With Sentry (error only) 6,184 67% 6,098 +1%
POST Baseline 1,187 - 1,166 +2%
POST With Sentry 595 50% 583 +2%
POST With Sentry (error only) 1,022 86% 1,022 -
MYSQL Baseline 3,148 - 3,176 -1%
MYSQL With Sentry 445 14% 433 +3%
MYSQL With Sentry (error only) 2,590 82% 2,569 +1%

View base workflow run

Copy link
Copy Markdown
Member

@nicohrubec nicohrubec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, lgtm!

Copy link
Copy Markdown
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to see some usage of span links :)

const storedContext: StoredSpanContext = {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
sampled: spanContext.traceFlags === TraceFlags.SAMPLED,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: we have a spanIsSampled helper in core, let's use that instead

traceFlags: storedContext.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE,
},
attributes: {
'sentry.link.type': 'previous_trace',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: We can use SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE from core instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudflare Instrument Alarm Api Cloudflare alarm split in different traces

4 participants